<?php

abstract class Soul_Model_Entity_Base extends Soul_Object implements Soul_Event_Interface_Dispatcher{

    protected static $initializedProperties = array();
    
    private $_db = NULL;
    
    protected $tableName = "";
    protected $dbDriver = NULL;
    protected $originalData = array();
    private $_isNew = TRUE;
    private $_changedProperties = array();
    protected $properties = array();

    public function __construct() {
        parent::__construct();
        $this->initialize();
    }
    
    protected function initialize(){
        if (!isset(self::$initializedProperties[get_class($this)])) {
            $this->initializeProperties();
            self::$initializedProperties[get_class($this)] = $this->properties;
        }
        $this->properties = self::$initializedProperties[get_class($this)];
        $this->markNew();
    }

    protected abstract function initializeProperties();
    
    public function getDb(){
        if(is_null($this->_db)){
            $this->_db = Soul_Db_Factory::getDriver($this->dbDriver);
        }
        return $this->_db;
    }
    
    public function setDb($value){
        $this->_db = $value;
        return $this;
    }
    
    protected function checkExists(array $values){
    	$q = $this->getQuery();
    	foreach($values as $n=>$v){
    		$p = $this->getProperty($n);
    		$q->where('`'.$p->getName().'`=:'.$p->getName().':s');
    		$q->addParameter($p->getName(), $v);
    	}
    	return $q->fetchCount() > 0;    	
    }
    
    public function getQuery(){
    	$result = new Soul_Model_Query();
    	return $result->setModelClass(get_class($this))->from($this->getTableName());
    }
    
    public function getUpdate($changed = TRUE){
    	$result = Soul_Db_Factory::createUpdate();
    	$result->update($this->getTableName());
        foreach($this->getChangedProperties() as $property){
    		if($property->getUpdateable()){
    			$result->set($property->getName(), 
    				$property->convertToDb($this->getData($property->getName())),
    				$property->getParameterType());
    		}
    	}
    	return $result; 
    }
    
    public function getTableName(){
    	return $this->tableName;
    }
    
    public function setData($name, $value) {
//    	if(!$this->isNew()){
    	$property = $this->getProperty($name);
    	if(!is_null($property)){
    		if(!in_array($property, $this->_changedProperties)){
	    		if(!$property->compareValue($this->getData($name), $value)){
	    			$this->_changedProperties[$name] = $property;
		    	}
    		}
    	}
//    	}
    	return parent::setData($name, $value);
    }    
    
    public function getProperties(){
    	return $this->properties;
    }
    
    public function hasProperty($name){
    	return $this->getProperty($name) != NULL;
    }
    
    public function getIdProperty() {
        foreach ($this->properties as $property){
        	if($property->getIsPrimary()){
        		return $property;
        	}
        }
        return NULL;
    }

    public function getProperty($name){
        foreach ($this->properties as $property){
        	if($property->getName() == $name){
        		return $property;
        	}
        }
        return NULL;    	
    }    
    
    protected function addProperty(Soul_Model_Property_Base $value){
    	$this->properties[$value->getName()] = $value;
    	return $this;
    }
    
    protected function newProperty($name, $type = 'string', $primaryKey = FALSE, $increment = FALSE){
    	$result = NULL;
    	switch(strtolower($type)){
    		case 'string':
    			$result = new Soul_Model_Property_String();
    			break;
    		case 'integer':
    			$result = new Soul_Model_Property_Integer();
    			break;
    		case 'float':
    			$result = new Soul_Model_Property_Float();
    			break;    			   			
    		case 'datetime':
    			$result = new Soul_Model_Property_Datetime();
    			break;
            case 'timestamp':
    			$result = new Soul_Model_Property_Timestamp();
    			break; 
            case 'boolean':
    			$result = new Soul_Model_Property_Boolean();
    			break;        
    		case 'serialize':
    			$result = new Soul_Model_Property_Serialize();
    			break;    			
    		case 'array':
    			$result = new Soul_Model_Property_Array();
    			break;
    		case 'email':
    			$result = new Soul_Model_Property_Email();
    			break;             
    		case 'guid':
    			$result = new Soul_Model_Property_Guid();
    			break;     			    			
    	}
    	return $result->setName($name)->setIsPrimary($primaryKey)->setIsIncrement($increment);
    }
    
    protected function createProperty($name, $type = 'string', $primaryKey = FALSE, $increment = FALSE){
        $result = $this->newProperty($name, $type, $primaryKey, $increment);
    	$this->addProperty($result);
    	return $result;
    }    
    
    public function markOld(){
    	$this->_isNew = FALSE;
    	return $this;
    }   

    public function markNew(){
    	$this->_isNew = TRUE;
    	return $this;
    }     

    public function markNoChanged() {
    	$this->_isNew = FALSE;
		$this->_changedProperties = array();
        return $this;
    }

    public function isChanged() {
        if ($this->isNew()) {
            return TRUE;
        }
        return count($this->_changedProperties) > 0;
    }

    public function getOriginalData($name) {
        if (isset($this->originalData[$name])) {
            return $this->originalData[$name];
        }
        return NULL;
    }

    protected function propertyIsChanged($name) {
		if($this->hasProperty($name)){
			foreach($this->getChangedProperties() as $property){
				if($property->getName() == $name){
					return TRUE;
				}
			}
		}
        return FALSE;
    }

    public function getChangedData() {
        $result = array();
        foreach ($this->getChangedProperties() as $property) {
        	$result[$property->getName()] = $property->getEntityData($this);
        }
        return $result;
    }
    
    public function getChangedProperties(){
//		if(count($this->_changedProperties) > 0){
//            $result = $this->_changedProperties;
//            $timeTypes = array('datetime', 'timestamp');
//            foreach($this->getProperties() as $property){
//                $t = $property->getPropertyType();
//                if(!isset($this->_changedProperties[$property->getName()]) &&
//                        in_array($t, $timeTypes) && 
//                        $property->getAutoRefresh() &&
//                        $property->getUpdateable()){  
//                    $result[$property->getName()] = $property;
//                }
//            }
//            return $result;
//        }
        return $this->_changedProperties;
    }

    public function getIdValue() {
        return $this->getData($this->getIdProperty()->getName(), NULL);
    }

    public function setIdValue($value) {
        $this->setData($this->getIdProperty()->getName(), $value);
        return $this;
    }

    public function isNew() {
    	return $this->_isNew;
    }

    public function validate() {
    	return $this;
    }
    
    public function merge($id = NULL){
    	$nd = $this->getAllData();
    	if(is_null($id)){
    		$id = $this->getIdValue();
    	}
    	if(!empty($id)){
    		$this->load($id)->addData($nd);
    	}
    	return $this;
    }     

    public function save() {
        $this->isNew() ? $this->insert() : $this->update();
        return $this;
    }

    public function insert() {
        $this->beforeInsert();
        $this->insertSelf();
        $this->afterInsert();
        $this->markNoChanged();
        return $this;
    }

    protected function beforeInsert() {}
    
    protected function insertSelf() {
    	$fields = array();
    	$values = array();
    	$params = array();
    	foreach($this->getProperties() as $property){
    		if($property->getInsertable()){
    			$fields[] = '`'.$property->getName().'`';
    			$values[] = ':'.$property->getName().':'.$property->getParameterType();
    			$params[$property->getName()] = $property->convertToDb($this->getData($property->getName()));
    		}
    	}
    	$sql = 'INSERT INTO `'.$this->getTableName().'` ('.implode(',', $fields).') VALUES ('.implode(',', $values).')';
    	$this->getDb()->setSql($sql, $params)->execute();
    	if($this->getIdProperty()->getIsIncrement()){
    		$this->setIdValue($this->getDb()->getInsertId());
    	}
    }

    protected function afterInsert() {}   

    public function update() {
        $this->beforeUpdate();
        if ($this->isChanged()) {
            $this->updateSelf();
        }
        $this->afterUpdate();
        $this->markNoChanged();
        return $this;
    }

    protected function updateSelf() {
    	$values = array();
    	$params = array();
    	foreach($this->getChangedProperties() as $property){
    		if($property->getUpdateable()){
    			$params[$property->getName()] = $property->convertToDb($this->getData($property->getName()));
    			$values[] = '`'.$property->getName().'`=:'.$property->getName().':'.$property->getParameterType();
    		}
    	}
    	$idProperty = $this->getIdProperty();
    	$params[$idProperty->getName()] = $this->getIdValue();
    	$sql = 'UPDATE `'.$this->getTableName().'` SET '.implode(',', $values);
    	$sql .= ' WHERE `'.$idProperty->getName().'`=:'.$idProperty->getName().':'.$idProperty->getParameterType();
    	$this->getDb()->setSql($sql, $params)->execute();
    }

    protected function beforeUpdate() {}

    protected function afterUpdate() {}

    public function delete() {
    	$this->beforeDelete();
        $this->deleteSelf();
        $this->afterDelete();
        return $this;
    }
    
    protected function deleteSelf(){
    	$idProperty = $this->getIdProperty();
    	$sql = 'DELETE FROM `'.$this->getTableName().'`';
    	$sql .= 'WHERE `'.$idProperty->getName().'`=:'.$idProperty->getName().':'.$idProperty->getParameterType();
		$this->getDb()->setSql($sql, array($idProperty->getName()=>$this->getIdValue()))->execute();   	
    }

    protected function beforeDelete() {}

    protected function afterDelete() {}
    
    public function updateBatch(array $ids){
    	$values = array();
    	$params = array();
    	foreach($this->getChangedProperties() as $property){
    		if($property->getUpdateable()){
    			$params[$property->getName()] = $property->convertToDb($this->getData($property->getName()));
    			$values[] = '`'.$property->getName().'`=:'.$property->getName().':'.$property->getParameterType();
    		}
    	}
    	$idProperty = $this->getIdProperty();
    	$params[$idProperty->getName()] = $ids;
    	$sql = 'UPDATE `'.$this->getTableName().'` SET '.implode(',', $values);
    	$sql .= ' WHERE `'.$idProperty->getName().'` IN(:'.$idProperty->getName().':a)';
    	$this->getDb()->setSql($sql, $params)->execute();
    	return $this;
    }
    
    public function deleteBatch(array $ids){
    	$idProperty = $this->getIdProperty();
    	$sql = 'DELETE FROM `'.$this->getTableName().'`';
    	$sql .= 'WHERE `'.$idProperty->getName().'` IN(:'.$idProperty->getName().':a)';
		$this->getDb()->setSql($sql, array($idProperty->getName()=>$ids))->execute();
		return $this;   	
    }    

    public function equal($obj) {
        if (empty($obj)) {
            return FALSE;
        }
        if (get_class($obj) != get_class($this)) {
            return FALSE;
        }
        return $this->equalId($obj->getIdValue());
    }

    public function equalId($id) {
        $meId = $this->getIdValue();
        if (empty($meId)) {
            return FALSE;
        }

        return strcmp($meId, $id) == 0;
    }
    
    public function fill($data){
    	foreach($data as $name=>$value){
    		$isFound = FALSE;
    		foreach($this->getProperties() as $property){
    			if($property->getName() == $name){
                    $cv = $property->convertFromDb($value);
    				$this->setData($property->getName(), $cv);
    				$this->originalData[$property->getName()] = $cv;
    				$isFound = TRUE;
    				break;
    			}
    		}
    		if(!$isFound){
    			$this->setData($name, $value);
    		}
    	}
    	return $this->markNoChanged();
    }

    public function load($id) {
    	$idProperty = $this->getIdProperty();
    	$sql = 'SELECT * FROM `'.$this->getTableName().'`';
    	$sql .= 'WHERE `'.$idProperty->getName().'`=:'.$idProperty->getName().':'.$idProperty->getParameterType();
    	
    	$data = $this->getDb()->setSql($sql, array($idProperty->getName()=>$id))->fetch()->getFirstRow();
        if(!empty($data)){
    		$this->fill($data);
    	}
    	return $this;
    }
    
    public function loadBy(array $conditions){
        $q = $this->getQuery();
        foreach($conditions as $property=>$value){
            $q->condition($property, $value);
        }
        $data = $q->fetch()->getFirstRow();
        if(!is_null($data)){
            $this->fill($data);
        }
        return $this;
    }
    
	public function dispatchEvent($name, array $arguments = array()){
		Soul_Event_Manager::callReceiver($name, new Soul_Event_Argument($this, $arguments));
	}    
}